--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit b3960cc4a420b90a1ccf9006dd3d93f65f11fa9f
Parents : db40ee7
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-06-05T22:44:08-05:00
feat(database): update SQLite connection configuration with busy timeout and WAL mode, improving concurrency handling
Changes
2 files changed, 68 insertions(+), 43 deletions(-)
Diff
diff --git a/meshchatx/src/backend/database/provider.py b/meshchatx/src/backend/database/provider.py
index f5a58bcb..3f686e40 100644
--- a/meshchatx/src/backend/database/provider.py
+++ b/meshchatx/src/backend/database/provider.py
@@ -9,6 +9,8 @@ _SQLITE_CONNECT_KW = {}
if sys.version_info >= (3, 14):
_SQLITE_CONNECT_KW["cached_statements"] = 100
+_SQLITE_BUSY_TIMEOUT_MS = 5000
+
class DatabaseProvider:
_instance = None
@@ -21,6 +23,19 @@ class DatabaseProvider:
self._all_locals.add(self._local)
self._memory_connection = None
+ @staticmethod
+ def _configure_connection(connection):
+ if connection is None:
+ return
+ try:
+ connection.execute(f"PRAGMA busy_timeout={_SQLITE_BUSY_TIMEOUT_MS}")
+ except sqlite3.OperationalError:
+ pass
+ try:
+ connection.execute("PRAGMA journal_mode=WAL")
+ except sqlite3.OperationalError:
+ pass
+
@classmethod
def get_instance(cls, db_path=None):
with cls._lock:
@@ -50,6 +65,7 @@ class DatabaseProvider:
**_SQLITE_CONNECT_KW,
)
self._memory_connection.row_factory = sqlite3.Row
+ self._configure_connection(self._memory_connection)
return self._memory_connection
if not hasattr(self._local, "connection"):
@@ -62,13 +78,8 @@ class DatabaseProvider:
**_SQLITE_CONNECT_KW,
)
self._local.connection.row_factory = sqlite3.Row
- # Enable WAL mode for better concurrency
if self.db_path != ":memory:":
- try:
- self._local.connection.execute("PRAGMA journal_mode=WAL")
- except sqlite3.OperationalError:
- # Some environments might not support WAL
- pass
+ self._configure_connection(self._local.connection)
return self._local.connection
def execute(self, query, params=None, commit=None):
@@ -109,7 +120,9 @@ class DatabaseProvider:
def begin(self):
try:
- self.connection.execute("BEGIN")
+ # IMMEDIATE acquires a reserved lock up front so concurrent writers wait
+ # on busy_timeout instead of failing with "database is locked" at commit.
+ self.connection.execute("BEGIN IMMEDIATE")
except sqlite3.OperationalError as e:
if "within a transaction" in str(e):
pass
diff --git a/tests/backend/test_messages_export.py b/tests/backend/test_messages_export.py
index 46441867..8688a703 100644
--- a/tests/backend/test_messages_export.py
+++ b/tests/backend/test_messages_export.py
@@ -11,6 +11,51 @@ import RNS
from meshchatx.meshchat import ReticulumMeshChat
+def _make_json_request(body):
+ request = MagicMock()
+
+ async def _json():
+ return body
+
+ request.json = _json
+ return request
+
+
+def _make_multipart_file_request(body: bytes):
+ class _MultipartField:
+ name = "file"
+
+ def __init__(self, data):
+ self._data = data
+ self._offset = 0
+
+ async def read_chunk(self, size=1024 * 1024):
+ if self._offset >= len(self._data):
+ return b""
+ chunk = self._data[self._offset : self._offset + size]
+ self._offset += len(chunk)
+ return chunk
+
+ class _MultipartReader:
+ def __init__(self, field):
+ self._field = field
+ self._done = False
+
+ async def next(self):
+ if self._done:
+ return None
+ self._done = True
+ return self._field
+
+ request = MagicMock()
+
+ async def _multipart():
+ return _MultipartReader(_MultipartField(body))
+
+ request.multipart = _multipart
+ return request
+
+
@pytest.fixture
def temp_dir():
dir_path = tempfile.mkdtemp()
@@ -240,11 +285,9 @@ async def test_messages_import_export_roundtrip(mock_rns_minimal, temp_dir):
assert len(export_data["messages"]) == 1
assert "lxmf_icon" not in export_data["messages"][0]
- import_request = MagicMock()
- import_request.json = MagicMock(
- return_value={"messages": export_data["messages"]},
+ import_response = await import_handler(
+ _make_json_request({"messages": export_data["messages"]}),
)
- import_response = await import_handler(import_request)
assert import_response.status == 200
import_data = json.loads(import_response.body)
assert import_data["imported"] == 1
@@ -287,38 +330,7 @@ async def test_messages_import_file_upload(mock_rns_minimal, temp_dir):
],
}
body = json.dumps(payload).encode("utf-8")
-
- class _MultipartField:
- name = "file"
-
- def __init__(self, data):
- self._data = data
- self._offset = 0
-
- async def read_chunk(self, size=1024 * 1024):
- if self._offset >= len(self._data):
- return b""
- chunk = self._data[self._offset : self._offset + size]
- self._offset += len(chunk)
- return chunk
-
- class _MultipartReader:
- def __init__(self, field):
- self._field = field
- self._done = False
-
- async def next(self):
- if self._done:
- return None
- self._done = True
- return self._field
-
- request = MagicMock()
- request.multipart = MagicMock(
- return_value=_MultipartReader(_MultipartField(body)),
- )
-
- response = await import_file_handler(request)
+ response = await import_file_handler(_make_multipart_file_request(body))
assert response.status == 200
data = json.loads(response.body)
assert data["imported"] == 1
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────